﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Cofe.Core.Logger;
using Cofe.Core.Security;
using Cofe.Core.Service;
using Cofe.Core.Utils;
using Cofe.Core.Validater;

namespace Cofe.Core.Property
{
    public class PropertyBehaviorManager : IPropertyBehaviorManager
    {
        [Export(typeof(ICofeService))]
        [ServicePriority(ServicePriorityAttribute.DefaultPriority_COFE)]
        public class Factory : IPropertyBehaviorManagerFactory
        {

            public IPropertyBehaviorManager ConstructBehavior(IPropertyHost propertyHost, IServiceLocater serviceLocater)
            {
                return new PropertyBehaviorManager(propertyHost, serviceLocater);
            }
        }

        #region Constructor

        private void init(IServiceLocater serviceLocater)
        {
            _propertyDictionary = serviceLocater.FindService<IPropertyDictionary>();
        }

        public PropertyBehaviorManager(IPropertyHost propertyHost, IServiceLocater serviceLocater)
        {
            _propertyHost = propertyHost;
            init(serviceLocater);
        }

        #endregion

        #region Methods


        private static Dictionary<PropertyRetrivalOptions, T> loadBehaviors<T>()
            where T : IPropertyProviderBehavior
        {
            var retVal = new Dictionary<PropertyRetrivalOptions, T>();
            var allListBehavior = CofeServices.ServiceLocater.FindAllServices<T>();
            foreach (var name in Enum.GetNames(typeof(PropertyRetrivalOptions)))
            {
                PropertyRetrivalOptions option;
                Enum.TryParse<PropertyRetrivalOptions>(name, out option);
                var behavior = allListBehavior.FirstOrDefault(b => b.SupportOption.HasFlag(option));
                retVal.Add(option, behavior);
            }
            return retVal;
        }

        private static void loadBehaviors()
        {
            if (_listBehavior == null) _listBehavior = loadBehaviors<IPropertyListSupportedBehavior>();
            if (_getBehavior == null) _getBehavior = loadBehaviors<IPropertyGetBehavior>();
            if (_setBehavior == null) _setBehavior = loadBehaviors<IPropertySetBehavior>();
        }


        public async Task<object[]> GetSupportedPropertyAsync(PropertyRetrivalOptions options = PropertyRetrivalOptions.Default,
            MatchMode matchMode = MatchMode.All, params IPropertyFilter[] filters)
        {
            object[] supportedProperty;

            if (_propertyHost.PropertyCache != null &&
                _propertyHost.PropertyCache.IsPropertySupported(CacheProperties.SupportedProperties))
            {
                supportedProperty = (await _propertyHost.PropertyCache.GetOnePropertyAsync(CacheProperties.SupportedProperties))
                    .ValueAs<object[]>();

                var behavior = _listBehavior[options];
                if (behavior == null) throw new NotSupportedException("Behavior");
                var secondarysupportedProperty = behavior.GetSupportedProperty(_propertyHost.SecondaryPropertyProviders).ToArray();
                supportedProperty = supportedProperty.Union(secondarysupportedProperty).Distinct().ToArray();
            }
            else
            {
                await _propertyHost.ConnectAsync();
                loadBehaviors();
                var behavior = _listBehavior[options];
                if (behavior == null) throw new NotSupportedException("Behavior");
                supportedProperty = behavior.GetSupportedProperty(_propertyHost.AllPropertyProviders).ToArray();
            }

            return PropertyUtils.FilterProperty(supportedProperty.ToList(), matchMode, filters).ToArray();
        }


        public bool IsPropertySupported(object property)
        {
            return AsyncUtils.RunSync(() => GetSupportedPropertyAsync()).Contains(property);
        }

        public async Task<PropertyPair> GetPropertyAsync(object property, PropertyRetrivalOptions options = PropertyRetrivalOptions.Default)
        {
            try
            {
                if (property is string)
                {
                    object propertyObject = _propertyHost.ServiceLocater.FindService<IPropertyDictionary>().GetProperty((string)property);
                    return await GetPropertyAsync(propertyObject);
                }

                options = options != PropertyRetrivalOptions.Default ? options : PropertyUtils.GetPropertyRetrivalOptions(property);

                switch (PropertyHost.State)
                {
                    case PropertyHostState.Default:
                        CofeServices.SecurityManager.DemandPermission(
                                          this.PropertyHost,
                                          DecoratorExtension.GetRequirePermissionForRead(property)
                                          );
                        break;
                }



                loadBehaviors();
                var behavior = _getBehavior[options];
                if (behavior == null) throw new NotSupportedException("Behavior");

                #region If the property require connected, connect first.
                var propCacheOption = CofeServices.PropertyDictionary.AttributeHelper.GetPropertyCacheOption(property);
                if (propCacheOption.HasFlag(PropertyCacheOptions.RequireConnected))
                    await _propertyHost.ConnectAsync();
                #endregion

                var retVal = await behavior.GetPropertyAsync(property, _propertyHost.AllPropertyProviders, options);
                if (!property.Equals(CofeProperties.FullParseName) && retVal == null && _propertyHost.PrimaryPropertyProvider == null)
                {
                    CofeServices.Logger.Log("PBM Connecting PropertyHost because cannot find {0} from {1}", property, _propertyHost);
                    await _propertyHost.ConnectAsync();
                    retVal = await behavior.GetPropertyAsync(property, _propertyHost.AllPropertyProviders, options);
                }

                if (retVal != null && retVal.Any(pp => pp.Property.Equals(property)))
                {
                    //Update back to cache.
                    if (_propertyHost.State == PropertyHostState.Default && _propertyHost.PropertyCache.CanCache(property))
                        await _propertyHost.PropertyCache.CachePropertyAsync(retVal);
                    return retVal.First(pp => pp.Property.Equals(property));
                }


                throw new NotSupportedException(property.ToString());
            }
            catch (Exception ex)
            {
                if (_propertyHost.State == PropertyHostState.ORMUpdating) //Mute error if updating database.
                {
                    CofeServices.Logger.Log(ex);
                    var type = CofeServices.PropertyDictionary.AttributeHelper.GetDefaultType(property);
                    if (type == typeof(byte[]))
                        return PropertyPair.FromValue(property, new byte[] { });
                    return PropertyPair.FromValue(property, Activator.CreateInstance(type));
                }


                throw new Exception(String.Format("Error when getting {0} property", property), ex);
            }
        }

        public async Task<PropertyPair[]> GetPropertyAsync(object[] properties, PropertyRetrivalOptions options = PropertyRetrivalOptions.Default)
        {
            List<PropertyPair> retVal = new List<PropertyPair>();

            foreach (var p in properties)
                retVal.Add(await GetPropertyAsync(p, options));

            return retVal.ToArray();
        }

        public async Task<bool> SetPropertyAsync(PropertyPair propertyPair, PropertyRetrivalOptions options = PropertyRetrivalOptions.Default)
        {
            switch (PropertyHost.State)
            {
                case PropertyHostState.ORMUpdating:
                    if (!propertyPair.Property.Equals(CacheProperties.Id))
                        throw new InvalidOperationException("You cannot change a property when State = ORMUpdating.");
                    break;
                case PropertyHostState.ORMtoCache:
                    //Do nothing
                    await this.PropertyHost.PropertyCache.CachePropertyAsync(propertyPair);
                    return true;
                    //break;
                case PropertyHostState.Default:
                    CofeServices.SecurityManager.DemandPermission(
                        this.PropertyHost,
                        DecoratorExtension.GetRequirePermissionForWrite(propertyPair.Property)
                        );
                    break;
            }



            options = options != PropertyRetrivalOptions.Default ? options : PropertyUtils.GetPropertyRetrivalOptions(propertyPair.Property);
            if (PropertyHost.State == PropertyHostState.ORMtoCache)
                options = PropertyRetrivalOptions.CacheOnly;

            loadBehaviors();
            var behavior = _setBehavior[options];
            if (behavior == null) throw new NotSupportedException(string.Format( "Behavior for {0} is not exists", options));

            if (options != PropertyRetrivalOptions.First
                && options != PropertyRetrivalOptions.CacheOnly) //If First, then PropertyCache will be updated only.
                await _propertyHost.ConnectAsync(); //Have to write to main.
            bool retVal = await behavior.SetPropertyAsync(_propertyHost.AllPropertyProviders, propertyPair, options);

            return retVal;
        }


        public async Task<T> InvokeAsync<T>(object property, ParameterDic parameters = null)
        {
            if (parameters == null) parameters = new ParameterDic();
            parameters.PropertyHost = this.PropertyHost;

            if (!IsPropertySupported(property))
            {
                var relatedProperty = PropertyUtils.FindRelatedProperty(property);
                if (relatedProperty != null && IsPropertySupported(relatedProperty))
                    return Invoke<T>(relatedProperty, parameters);
                else throw new NotSupportedException();
            }

            PropertyPair ppair = await GetPropertyAsync(property);
            return await _propertyDictionary.PropertyInvoker.InvokeAsync<T>(ppair, parameters);
        }

        public async Task InvokeAsync(object property, ParameterDic parameters = null)
        {
            if (parameters == null) parameters = new ParameterDic();
            parameters.PropertyHost = this.PropertyHost;

            if (!IsPropertySupported(property))
            {
                var relatedProperty = PropertyUtils.FindRelatedProperty(property);
                if (relatedProperty != null && IsPropertySupported(relatedProperty))
                {
                    Invoke(relatedProperty, parameters);
                    return;
                }
                else throw new NotSupportedException();
            }

            PropertyPair ppair = await GetPropertyAsync(property);
            await _propertyDictionary.PropertyInvoker.InvokeAsync(ppair, parameters);

        }

        public T Invoke<T>(object property, ParameterDic parameters = null)
        {
            if (parameters == null) parameters = new ParameterDic();
            parameters.PropertyHost = this.PropertyHost;

            if (!IsPropertySupported(property))
            {
                var relatedProperty = PropertyUtils.FindRelatedProperty(property);
                if (relatedProperty != null && IsPropertySupported(relatedProperty))
                {
                    return Task.Run<T>(async () => await InvokeAsync<T>(relatedProperty, parameters)).Result;
                }

                else throw new NotSupportedException();
            }

            PropertyPair ppair = this.GetProperty(property);
            return _propertyDictionary.PropertyInvoker.Invoke<T>(ppair, parameters);
        }

        public void Invoke(object property, ParameterDic parameters = null)
        {
            if (parameters == null) parameters = new ParameterDic();
            parameters.PropertyHost = this.PropertyHost;

            if (!IsPropertySupported(property))
            {
                var relatedProperty = PropertyUtils.FindRelatedProperty(property);
                if (relatedProperty != null && IsPropertySupported(relatedProperty))
                {
                    InvokeAsync(relatedProperty, parameters).RunSynchronously();
                    return;
                }
                else throw new NotSupportedException();
            }

            PropertyPair ppair = this.GetProperty(property);
            _propertyDictionary.PropertyInvoker.Invoke(ppair, parameters);
        }

        public async Task<int> CommitChangesAsync()
        {
            int propCount = PropertyHost.PropertyCache.ChangedProperties.Count();
            if (propCount > 0)
            {
                var setter = CofeServices.ServiceLocater.FindService<SetIgnoreCache>();
                await PropertyHost.ConnectAsync();
                foreach (var prop in PropertyHost.PropertyCache.ChangedProperties)
                {
                    var pp = (await PropertyHost.PropertyCache.GetPropertyAsync(prop)).Single();

                    CofeServices.SecurityManager.DemandPermission(
                                      this.PropertyHost,
                                      DecoratorExtension.GetRequirePermissionForWrite(pp.Property)
                                      );

                    await setter.SetPropertyAsync(PropertyHost.AllPropertyProviders, pp, PropertyRetrivalOptions.Default);
                }
            }
            PropertyHost.PropertyCache.ResetChanges();
            return propCount;
        }

        public int ResetChanges()
        {
            var changedProperties = PropertyHost.PropertyCache.ChangedProperties.ToArray();
            PropertyHost.PropertyCache.ResetCache(p => changedProperties.Contains(p));
            return changedProperties.Count();
        }

        #endregion

        #region Data

        private IPropertyDictionary _propertyDictionary;
        private IPropertyHost _propertyHost;

        static Dictionary<PropertyRetrivalOptions, IPropertyListSupportedBehavior> _listBehavior;
        static Dictionary<PropertyRetrivalOptions, IPropertyGetBehavior> _getBehavior;
        static Dictionary<PropertyRetrivalOptions, IPropertySetBehavior> _setBehavior;

        #endregion

        #region Public Properties

        public IPropertyHost PropertyHost { get { return _propertyHost; } }

        #endregion















    }
}
